TypeScriptで関数型指向で抽象を扱うにはどうすれば良いのか @Kyoto.js 22
自己紹介
いつもの
~1:00
コーディング時の考えることを減らしたい
今書いているプロダクト
Next.js実装のto B向けの販売管理システム
約7万行
2024/6現在は主に一人で書いている
他2名に手伝ってもらっている時期もあり
どんどん抽象して、考えることを減らさないと暴発する
~2:00
TypeScriptで、どのように抽象を扱うかを考えている
今回は、その考えの経過と整理を共有します
(& 悩みの共有、という感じです...mrsekut.icon)
結論が出ているわけではないので、どんな意見でも頂けるとありがたいです
まとめ
業務で使っているTypeScriptでの抽象の扱いを模索していた
データ構造に着目してみた
→汎用的なデータ構造に逃してみたがあまり上手く行かず
操作に着目してみた
→データ構造の内部を知っているかどうかで分類できそう
「内部を知らない操作」が大事そう
→TypeScriptでの実現方法が不明 ←イマココmrsekut.icon
なにか知見があれば教えてください!
~2:30
データ構造に着目する
いわゆる、Making illegal states unrepresentable
そもそも間違った状態にならないように厳密に型の設計をする
例えば、決済方法を定義する
「銀行振込」「クレジットカード」の2種類がある
それぞれ必要となる属性は異なっている
https://gyazo.com/b4585ee12fa69af305538e714a4ecce9
こうしない
code:ts
type PaymentMethod = {
type: 'bankTransfer' | 'creditCard';
accountNumber?: AccountNumber;
bankName?: BankName;
cardNumber?: CardNumber;
cardHolder?: CardHolder;
}
nullableなものが多い
typeで分岐した後も、各属性の存在を確認する必要がある
こうする
code:ts
type PaymentMethod = BankTransferPayment | CreditCardPayment;
type BankTransferPayment = {
type: 'bankTransfer';
accountNumber: AccountNumber;
bankName: BankName;
};
type CreditCardPayment = {
type: 'creditCard';
cardNumber: CardNumber;
cardHolder: CardHolder;
};
型を見るだけで仕様が伝わる
「型エラーがなければ値としては正しい」ことが保証される
~4:45
一方で、どのようにして抽象を表すのだろう?
上記の書き方は、安全に寄与するが、具体的すぎる
例えば、A、B、Cのパターンを取る時に、
T = A | B | Cと定義するのは容易だが、
Tを扱う時に、毎回3つの条件分岐を書いているのは抽象化と言えるのか?
~5:45
1つ目のアプローチ: 汎用的なデータ構造に切り出す
「データ構造」を2つに分けて考えてみる
具体的なデータ構造
特定のドメインに特化しないようなデータ構造
例えば、
type Either<L, R> = Left<L> | Right<R>;
type Maybe<T> = Just<T> | Nothing;
type Array<T> = ...
Reducerに渡すEvent型
JotaiのAtomの型
etc.
Haskell視点でみるとMonadのinstanceになるような型?mrsekut.icon
具体的なデータ構造を最小にまで減らす
しかし実際は、あまりうまくいかないmrsekut.icon
Entityの構造に、そういった構造が現れることが少ない
無理やりやると厳密な型定義にできない
よくよく見てみると、汎用的なデータ構造にはフローのための構造が多いことに気づくmrsekut.icon
ドメインを表す構造ではなく、そういった構造を包むコンテナの意味合いが大きい
例えば、Either, Maybeはエラーを制御するためのデータ構造
→このアプローチは、求めているものと違いそう
~8:00
2つ目のアプローチ: 操作に着目する
データ構造ではなく、「操作」で抽象化する
外部から見れば、
内部のデータ構造はどうだって良い
決済方法を扱う時に、「銀行振込とクレカで別の属性を持っている」という知識は不要
共通の操作で扱える
支払う (pay)という操作は、「銀行振込」「クレジットカード」で共通している
そして、操作をインターフェースとして外部に公開する
https://gyazo.com/9e382ac6e546644eff6f59363421b3ab
~9:15
(余談) 操作だけを公開したいが、たぶんできなそう
データ構造は隠蔽したい
しかし、TypeScriptでは隠蔽する方法がない
黙ってclassを使え、というのはある
こんな感じのネストした構造があった時、
code:ts
type C = { b: B };
type B = { a: A };
type A = { value: number };
Cに依存しているものは、c.b.a.valueとすれば内部が見えてしまう
aやvalueを隠蔽することができない
全ての子の構造を知ってるのと同じ
インターフェースが広すぎる、カプセル化が全くできない
_で始めるなど運用でカバーするしかない
code:ts
const c: C = { b: { _a: { _value: 1 } } };
~10:15
操作を2種類に分けられそう
以下の2つに分けられる
データ構造の内部を知っている操作
データ構造の内部を知らない操作
データ構造の内部を知っている操作の例
code:ts
const pay = (method: PaymentMethod, info: PaymentInfo) => {
switch (method.type) {
case 'creditCard':
return ...;
case 'bankTransfer':
return ...;
}
}
関数の内部で、'creditCard'と'bankTransfer'がハードコードされている
この関数は、
「PaymentMethodが、CreditCardPaymentとBankTransferPaymentから成る」
ということを知っている
故に、関数とデータ構造は密に結合している
(先程のPaymentMethodの再掲)
code:ts
type PaymentMethod = CreditCardPayment | BankTransferPayment;
type CreditCardPayment = {
type: 'creditCard';
cardNumber: CardNumber;
cardHolder: CardHolder;
};
type BankTransferPayment = {
type: 'bankTransfer';
accountNumber: AccountNumber;
bankName: BankName;
};
~11:30
データ構造の内部を知らない操作の例
恐らく、TypeScriptではclassを使わないと実現できない...?mrsekut.icon
なにか実現できそうな方法があれば知りたい
classを使うと以下のように書ける
code:ts
interface IPaymentMethod { // 命名適当です
pay(info: PaymentInfo): string;
}
const pay_ = (method: IPaymentMethod, info: PaymentInfo) => {
return method.pay(info);
}
関数pay_は
IPaymentMethodにのみ依存しており、
PaymentMethodの内部構造は知らない
他の言語だと、例えばHaskellでは型クラスを使って以下のように書ける
code:hs
class IPaymentMethod p where
pay :: p -> PaymentInfo -> String
pay' :: (IPaymentMethod p) => p -> PaymentInfo -> String
pay' method info = pay method info
関数pay'は
PaymentMethodI型クラスを実装している型だけを要請しており
実際にpがどういうデータ構造なのかは知らない
良さ
データ構造と操作の結合が疎になる
~13:00
2種類の操作の使い分け
データ構造の内部を知っている操作
データ構造と操作が同じmoduleに属するならあり
密に結合するが、許容できる
データ構造が変わると、操作も修正が必要
だが、外部module視点では、操作がインターフェースになるので影響なし
データ構造の内部を知らない操作
操作が、複数moduleに跨るものなら、こちらのほうが良さそう
データ構造と操作の結合を疎にできる
操作は、複数moduleを跨ぐ汎用的な機能、と見なすことができる
あるあるだと、CRUDのsaveとかそういうやつ
まとめ
業務で使っているTypeScriptでの抽象の扱いを模索していた
データ構造に着目してみた
→汎用的なデータ構造に逃してみたがあまり上手く行かず
操作に着目してみた
→データ構造の内部を知っているかどうかで分類できそう
「内部を知らない操作」が大事そう
→TypeScriptでの実現方法が不明 ←イマココmrsekut.icon
なにか知見があれば教えてください!